#include "StdAfx.h"

#include "GameManager.h"
#include "Terrain.h"
#include "Camera.h"
#include "Shader.h"
#include "CLManager.h"
#include "ParticleSystem.h"

namespace {
    void makeMultiple(unsigned int localRange, unsigned int& globalRange)
    {
        if ( localRange == globalRange ) return;
        for (;;)
        {
            if ( (++globalRange) % localRange == 0 )
                return;
        }
    }

    void launchSimKernel(cl::CommandQueue& queue, cl::Kernel& kernel, unsigned int bufSize, unsigned int offset, unsigned int globalRange, unsigned int localRange,
        cl::BufferGL& renderBuffer, cl::Buffer& simBuffer, cl::Image2DGL& heightTexIn, cl::Image2DGL& heightTexOut, cl::Image2DGL& normalTex, 
        unsigned int activePtr, unsigned int numParticles, float dt, glm::ivec2 heightTexDim, glm::vec2 terrainDim)
    {
        unsigned int argNum = 0;
        kernel.setArg(argNum++, sizeof(renderBuffer), &renderBuffer); 
        kernel.setArg(argNum++, sizeof(simBuffer), &simBuffer);
        kernel.setArg(argNum++, sizeof(heightTexIn), &heightTexIn);
        kernel.setArg(argNum++, sizeof(heightTexOut), &heightTexOut);
        kernel.setArg(argNum++, sizeof(heightTexOut), &normalTex);
        kernel.setArg(argNum++, sizeof(unsigned int), &activePtr);
        kernel.setArg(argNum++, sizeof(unsigned int), &numParticles);
        kernel.setArg(argNum++, sizeof(float), &dt);
        kernel.setArg(argNum++, sizeof(unsigned int), &heightTexDim.x);
        kernel.setArg(argNum++, sizeof(unsigned int), &heightTexDim.y); 
        kernel.setArg(argNum++, sizeof(float), &terrainDim.x);
        kernel.setArg(argNum++, sizeof(float), &terrainDim.y);

        CLErr(queue.enqueueNDRangeKernel(kernel, cl::NDRange(activePtr), cl::NDRange(globalRange), cl::NDRange(localRange)));
    }
}

ParticleSystem::ParticleSystem() : dim(64), activePtr(0), freePtr(0), bufSize(1<<20), maxTtl(100000)
{
    glEnable(GL_VERTEX_PROGRAM_POINT_SIZE);
    glPointSize(5.0f);

    cl_int err = CL_SUCCESS;
    clManager = CLManager::getInstance();    
    gm = GameManager::getInstance();

    installKernels();

    initVBO();

    shader = new Shader();
    shader->addStage("./shaders/particle.vert", "", GL_VERTEX_SHADER);
    shader->addStage("./shaders/particle.geom", "", GL_GEOMETRY_SHADER);
    shader->addStage("./shaders/particle.frag", "", GL_FRAGMENT_SHADER);
    shader->install();
}

ParticleSystem::~ParticleSystem()
{
}

void ParticleSystem::installKernels() 
{
    cl_int err;
    program = clManager->build("./kernels/particles.cl");
    particleKernel = cl::Kernel(program, "firstKernel", &err);
    CLErr(err);
    copyKernel = cl::Kernel(program, "copyKernel", &err);
    CLErr(err);
}

void ParticleSystem::initVBO()
{
    glGenVertexArrays(1, &vao);
    glGenBuffers(1, &glBuf);

    glBindVertexArray(vao);
    glBindBuffer(GL_ARRAY_BUFFER, glBuf);
    glBufferData(GL_ARRAY_BUFFER, bufSize * sizeof(ParticleVertex), nullptr , GL_STATIC_DRAW);

    unsigned int offset = 0;
    glVertexAttribPointer(semantic::attr::PARTICLE_POSITION, 4, GL_FLOAT, GL_FALSE, sizeof(ParticleVertex), BUFFER_OFFSET(offset));
    offset += sizeof(glm::vec4);
    glVertexAttribPointer(semantic::attr::PARTICLE_COLOR, 4, GL_FLOAT, GL_FALSE, sizeof(ParticleVertex), BUFFER_OFFSET(offset));
    offset += sizeof(glm::vec4);
    glVertexAttribPointer(semantic::attr::PARTICLE_PROP, 4, GL_FLOAT, GL_FALSE, sizeof(ParticleVertex), BUFFER_OFFSET(offset));

    glEnableVertexAttribArray(semantic::attr::PARTICLE_POSITION);
    glEnableVertexAttribArray(semantic::attr::PARTICLE_COLOR);
    glEnableVertexAttribArray(semantic::attr::PARTICLE_PROP);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    cl_int err = CL_SUCCESS;

    renderBuffer = cl::BufferGL(clManager->context, CL_MEM_READ_WRITE, glBuf, &err);
    CLErr(err);
    
    simBuffer = cl::Buffer(clManager->context, CL_MEM_READ_WRITE /*| CL_MEM_COPY_HOST_PTR*/, bufSize*sizeof(ParticleSim), nullptr, &err);
    CLErr(err);
} 

void ParticleSystem::addParticles(const glm::vec3& pos, float posRandomness, const glm::vec3& speed, float speedRandomness, float ttl, unsigned int count)
{
    std::vector<ParticleVertex> renderParticles(count);
    std::vector<ParticleSim> simParticles(count);

    for (size_t i = 0; i < count; ++i)
    {
        renderParticles[i].pos = glm::vec4(pos.x, pos.y, pos.z, 1.0f) 
            + glm::vec4(glm::vec4(randNormNegPos()*posRandomness, randNormNegPos()*posRandomness, randNormNegPos()*posRandomness, 0.0f));

        simParticles[i].speed = glm::vec4(speed.x, speed.y, speed.z, 1.0f) 
            + glm::vec4(randNormNegPos()*speedRandomness, randNormNegPos()*speedRandomness, randNormNegPos()*speedRandomness, 0.0f);

        renderParticles[i].color = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f);
        renderParticles[i].props = glm::vec4(0.4, 0.0f, ttl, /*unused*/0.0f);
    }

    { // upload particles to opencl buffers 
        CLAquire autoAquire(&clManager->queue());
        autoAquire.push(&renderBuffer());

        if ( freePtr + count > bufSize )
        {
            // wraps: |           after->|                              before->|          | 
            size_t after = (freePtr + count) % bufSize; 
            size_t before = count - after;

            CLErr(clManager->queue.enqueueWriteBuffer(simBuffer, true, freePtr*sizeof(ParticleSim), before*sizeof(ParticleSim), &simParticles[0]));
            CLErr(clManager->queue.enqueueWriteBuffer(renderBuffer, true, freePtr*sizeof(ParticleVertex), before*sizeof(ParticleVertex), &renderParticles[0]));

            CLErr(clManager->queue.enqueueWriteBuffer(simBuffer, true, 0, after*sizeof(ParticleSim), &simParticles[before]));
            CLErr(clManager->queue.enqueueWriteBuffer(renderBuffer, true, 0, after*sizeof(ParticleVertex), &renderParticles[before]));
        }
        else
        { // no-wrap
            CLErr(clManager->queue.enqueueWriteBuffer(simBuffer, true, freePtr*sizeof(ParticleSim), count*sizeof(ParticleSim), &simParticles[0]));
            CLErr(clManager->queue.enqueueWriteBuffer(renderBuffer, true, freePtr*sizeof(ParticleVertex), count*sizeof(ParticleVertex), &renderParticles[0]));
        }
    } // end opencl processing 
    freePtr += count;
    freePtr %= bufSize;
    particleStoreList.push_back(ParticleStore(ttl /*maxTtl*/, count));
}

void ParticleSystem::update(float dt)
{
#if 0
    static bool first = true;
    if ( first ) 
    {
        addParticles(glm::vec4(10.0f, 0.0f, 20.0f, 0.0f), 5.0f, 128);
        addParticles(glm::vec4(10.0f, 0.0f, 40.0f, 0.0f), 5.0f, 64);
        addParticles(glm::vec4(10.0f, 0.0f, 60.0f, 0.0f), 5.0f, 128);
        addParticles(glm::vec4(10.0f, 0.0f, 80.0f, 0.0f), 5.0f, 64);
        
        first = false;
    }
#endif 

    if (particleStoreList.empty())
        return;

    for (auto it = particleStoreList.begin(); it != particleStoreList.end(); ++it)
    {
        (*it).ttl -= dt;
    }

    // is the oldest region of active particles expired. 
    if ( particleStoreList.front().ttl < 0.0 ) 
    {
        activePtr += particleStoreList.front().count; // bump the active index
        activePtr %= bufSize;
        particleStoreList.pop_front(); // forget the particles 
#if 0 // debug 
        std::printf("removing particles: ");
        std::printf("ActivePtr:%i ", activePtr);
        std::printf("FreePtr:%i\n", freePtr);
#endif 
    }
}

void ParticleSystem::render(float dt) 
{
  /*  if ( activePtr == freePtr ) 
        return;*/

    // SIMULATE with OpenCL 

    cl_int err = CL_SUCCESS;
    glm::ivec2 heightTexDim = gm->terrain->getInputTex().getDim();

    {
        cl::Image2DGL heightTexIn = gm->terrain->getInputTex().getCL();  
        cl::Image2DGL heightTexOut = gm->terrain->getOutputTex().getCL();
        cl::Image2DGL normalTex = gm->terrain->getNormalTex().getCL();   
        CLAquire autoAquire(&clManager->queue());
        autoAquire.push(&renderBuffer()); autoAquire.push(&heightTexIn()); autoAquire.push(&heightTexOut()); autoAquire.push(&normalTex());

        {  // make the input and output height texture the same.. Necessary?
            size_t argNum = 0;
            copyKernel.setArg(argNum++, sizeof(heightTexIn), &heightTexIn);
            copyKernel.setArg(argNum++, sizeof(heightTexOut), &heightTexOut);
            copyKernel.setArg(argNum++, sizeof(unsigned int), &heightTexDim.x);
            copyKernel.setArg(argNum++, sizeof(unsigned int), &heightTexDim.y);

            assert(heightTexDim.x%16==0 && heightTexDim.y%16==0);
            CLErr(clManager->queue.enqueueNDRangeKernel(copyKernel, cl::NullRange, cl::NDRange(heightTexDim.x, heightTexDim.y, 1), cl::NDRange(16, 16, 1)));
        }
        {
            float terrainWidth  = gm->terrain->getWidth();
            float terrainHeight = gm->terrain->getHeight();
            glm::vec2 terrainDim(gm->terrain->getWidth(), gm->terrain->getHeight());
            if ( freePtr < activePtr ) // region of active particles wrap 
            {
                { // before
                    unsigned int numParticles = bufSize - activePtr;

                    unsigned int localRange = 128;
                    unsigned int globalRange = bufSize - activePtr;
                    makeMultiple(localRange, globalRange);

                    launchSimKernel(clManager->queue, particleKernel, bufSize, activePtr, globalRange, localRange, renderBuffer, simBuffer, 
                        heightTexIn, heightTexOut, normalTex, activePtr, numParticles, dt, heightTexDim, terrainDim);
                }
                { // wrapped
                    unsigned int numParticles = freePtr;
                    if ( numParticles > 0 ) // in case of freePtr = 0, no need to render wrapped 
                    {
                        size_t argNum = 0;
                        unsigned int zero = 0;

                        unsigned int localRange = 128;
                        unsigned int globalRange = freePtr;
                        makeMultiple(localRange, globalRange);

                        launchSimKernel(clManager->queue, particleKernel, bufSize, activePtr, globalRange, localRange, renderBuffer, simBuffer, 
                            heightTexIn, heightTexOut, normalTex, zero, numParticles, dt, heightTexDim, terrainDim);
                    }
                }
            }
            else // region of active particles does not wrap 
            {
                unsigned int numParticles = freePtr - activePtr;
                if ( numParticles > 0 )
                {
                    size_t argNum = 0;

                    unsigned int localRange = 128;
                    unsigned int globalRange = freePtr - activePtr;
                    makeMultiple(localRange, globalRange);

                    launchSimKernel(clManager->queue, particleKernel, bufSize, activePtr, globalRange, localRange, renderBuffer, simBuffer, 
                        heightTexIn, heightTexOut, normalTex, activePtr, numParticles, dt, heightTexDim, terrainDim);
                }
            }
        } // end particle sim 
    } // OpenCL processing complete  
    gm->terrain->swapTex();
    
    // RENDER with OpenGL, render from 'activePtr' to 'freePtr' 

    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glDepthMask(GL_FALSE);
    shader->begin(); 
    {
        int viewLoc = shader->getUniLoc("view");
        int projLoc = shader->getUniLoc("proj");
        glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(gm->activeCamera->getViewMatrix()));
        glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(gm->activeCamera->getProjMatrix()));

        glBindVertexArray(vao);
        glBindBuffer(GL_ARRAY_BUFFER, glBuf);

        if ( freePtr < activePtr ) 
        {
            glDrawArrays(GL_POINTS, activePtr, bufSize - activePtr);
            if ( freePtr > 0 )
                glDrawArrays(GL_POINTS, 0, freePtr);
        }
        else
        {
            if ( freePtr - activePtr > 0 ) 
                glDrawArrays(GL_POINTS, activePtr, freePtr - activePtr);
        }
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindVertexArray(0);
    } shader->end();
    glDepthMask(GL_TRUE);
    glDisable(GL_BLEND);
}